package in.co.codedoc.time;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Map;
import java.util.TreeMap;

public class Day
	implements Comparable<Day>
{
	public Day()
	{
		dayNumber = GetDayNumber(new GregorianCalendar());
	}
	
	public Day(int year,int month,int day)
	{
		dayNumber = GetDayNumber(new GregorianCalendar(year,month-1,day));
	}
	
	public Day(int dayNumber)
	{
		this.dayNumber = dayNumber;
	}

	public Day(Calendar day)
	{
		this.dayNumber = GetDayNumber(day);
	}
	
	public int GetDayNumber()
	{
		return dayNumber;
	}
	
	public int DaysSince(Day day)
	{
		return dayNumber - day.dayNumber;
	}
	
	public Day Add(int days)
	{
		return new Day(dayNumber + days);
	}
	
	public Day Increment(int days)
	{
		dayNumber += days;
		
		return this;
	}
	
	public boolean IsBefore(Day that)
	{
		return dayNumber < that.dayNumber;
	}
	
	public boolean IsAfter(Day that)
	{
		return dayNumber > that.dayNumber;
	}
	
	@Override
	public boolean equals(Object t)
	{
		if(t == null || !(t instanceof Day))
		{
			return false;
		}
		
		Day that = (Day)t;
		
		return that.dayNumber == dayNumber;
	}
	
	@Override
	public int hashCode()
	{
		return dayNumber;
	}
	
	@Override
	public String toString()
	{
		StringBuffer buffer = new StringBuffer();
		
		buffer.append(GetYear())
				.append('/')
				.append(GetMonth())
				.append('/')
				.append(GetDay());
				
		return buffer.toString();
	}
	
	@Override
	public int compareTo(Day that)
	{
		return
			dayNumber < that.dayNumber
			?
				-1
			:
				(
					dayNumber > that.dayNumber
					?
						+1
					:
						0
				)
			;
	}
	
	public int GetDay()
	{
		EnsureEncodedDate();
		
		return (encodedDate % 100);
	}
	
	public int GetMonth()
	{
		EnsureEncodedDate();
		
		return ((encodedDate/100) % 100);
	}
	
	public int GetYear()
	{
		EnsureEncodedDate();
		
		return (encodedDate/10000);
	}
	
	private void EnsureEncodedDate()
	{
		if(encodedDate == -1)
		{
			//Guess the year;
			int guessedYear = day0.get(Calendar.YEAR) + (dayNumber / 365);
	
			//Though this looks bad - it will end really soon
			int offset = 0;
			Calendar day = null;
			int year = 0;
			while(day == null)
			{
				if
				(
					(GetFirstDayOffset(guessedYear + offset) <=  dayNumber)
					&&
					(GetFirstDayOffset(guessedYear + offset + 1) >=  dayNumber)
				)
				{
					day = new GregorianCalendar(guessedYear + offset,Calendar.JANUARY,1);
					year = guessedYear + offset;
				}
				else if
				(
					(GetFirstDayOffset(guessedYear - offset) <=  dayNumber)
					&&
					(GetFirstDayOffset(guessedYear - offset + 1) >=  dayNumber)
				)
				{
					day = new GregorianCalendar(guessedYear - offset,Calendar.JANUARY,1);
					year = guessedYear - offset;
				}
				
				if(day == null)
				{
					offset++;
				}
				else
				{
					day.set(Calendar.DAY_OF_YEAR, (dayNumber - GetFirstDayOffset(year)));
				}
			}
			
			encodedDate = day.get(Calendar.YEAR) * 10000 + (1+ day.get(Calendar.MONTH)) * 100 + day.get(Calendar.DAY_OF_MONTH);
		}
	}

	public static final int GetDayNumber(Calendar day)
	{
		return DaysBetween(day,day0);
	}

	public Calendar GetCalendar()
	{
		return new GregorianCalendar(GetYear(),GetMonth()-1,GetDay());
	}
	
	private static final int DaysBetween(Calendar laterDay, Calendar earlierDay)
	{
		return DaysSinceDay0(laterDay) - DaysSinceDay0(earlierDay) + 1;
	}

	private static final int DaysSinceDay0(Calendar day)
	{
		return GetFirstDayOffset(day.get(Calendar.YEAR)) + day.get(Calendar.DAY_OF_YEAR);
	}

	private static int GetFirstDayOffset(int year)
	{
		synchronized(Day.class)
		{
			if(!firstDayOffsetByYear.containsKey(year))
			{
				int lowerYear;
				int higherYear;
				int multiplier = 1;
				if(year < day0.get(Calendar.YEAR))
				{
					lowerYear = year;
					higherYear = day0.get(Calendar.YEAR);
					multiplier = -1;
				}
				else
				{
					higherYear = year;
					lowerYear = day0.get(Calendar.YEAR);
				}
				
				int days = 0;
				for(int current = lowerYear;current < higherYear;++current)
				{
					days 
						+= new GregorianCalendar(current,Calendar.JANUARY,1)
							.getActualMaximum(Calendar.DAY_OF_YEAR);
				}
				
				firstDayOffsetByYear.put(year, multiplier*days);
			}
			
			return firstDayOffsetByYear.get(year);
		}
	}

	private int dayNumber;
	private int encodedDate = -1;
	private static Map<Integer, Integer> firstDayOffsetByYear = new TreeMap<Integer, Integer>();
	private static final Calendar day0 = new GregorianCalendar(1900,0,1);
}
